abstract-3d 4.1.6 → 4.2.1

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.
Files changed (60) hide show
  1. package/lib/abstract-3d.d.ts +63 -6
  2. package/lib/abstract-3d.d.ts.map +1 -1
  3. package/lib/abstract-3d.js +277 -74
  4. package/lib/abstract-3d.js.map +1 -1
  5. package/lib/renderers/dxf/dxf-encoding/dxf-dimension.d.ts +2 -2
  6. package/lib/renderers/dxf/dxf-encoding/dxf-dimension.d.ts.map +1 -1
  7. package/lib/renderers/dxf/dxf-encoding/dxf-dimension.js +6 -6
  8. package/lib/renderers/dxf/dxf-encoding/dxf-dimension.js.map +1 -1
  9. package/lib/renderers/dxf/dxf-geometries/dxf-dimension.d.ts +2 -2
  10. package/lib/renderers/dxf/dxf-geometries/dxf-dimension.d.ts.map +1 -1
  11. package/lib/renderers/dxf/dxf-geometries/dxf-dimension.js +3 -3
  12. package/lib/renderers/dxf/dxf-geometries/dxf-dimension.js.map +1 -1
  13. package/lib/renderers/dxf/dxf.d.ts.map +1 -1
  14. package/lib/renderers/dxf/dxf.js +12 -8
  15. package/lib/renderers/dxf/dxf.js.map +1 -1
  16. package/lib/renderers/ifc/ifc.d.ts.map +1 -1
  17. package/lib/renderers/ifc/ifc.js +5 -3
  18. package/lib/renderers/ifc/ifc.js.map +1 -1
  19. package/lib/renderers/step/step.d.ts.map +1 -1
  20. package/lib/renderers/step/step.js +3 -2
  21. package/lib/renderers/step/step.js.map +1 -1
  22. package/lib/renderers/stl/stl.d.ts.map +1 -1
  23. package/lib/renderers/stl/stl.js +7 -3
  24. package/lib/renderers/stl/stl.js.map +1 -1
  25. package/lib/renderers/svg/svg-geometries/svg-text.d.ts +1 -1
  26. package/lib/renderers/svg/svg-geometries/svg-text.d.ts.map +1 -1
  27. package/lib/renderers/svg/svg-geometries/svg-text.js +2 -2
  28. package/lib/renderers/svg/svg-geometries/svg-text.js.map +1 -1
  29. package/lib/renderers/svg/svg.d.ts.map +1 -1
  30. package/lib/renderers/svg/svg.js +13 -10
  31. package/lib/renderers/svg/svg.js.map +1 -1
  32. package/lib/renderers/svg-bsp/svg-geometries/svg-text.d.ts +1 -1
  33. package/lib/renderers/svg-bsp/svg-geometries/svg-text.d.ts.map +1 -1
  34. package/lib/renderers/svg-bsp/svg-geometries/svg-text.js +2 -2
  35. package/lib/renderers/svg-bsp/svg-geometries/svg-text.js.map +1 -1
  36. package/lib/renderers/svg-bsp/svg.d.ts.map +1 -1
  37. package/lib/renderers/svg-bsp/svg.js +13 -10
  38. package/lib/renderers/svg-bsp/svg.js.map +1 -1
  39. package/lib/renderers/three/build/build.js +1 -1
  40. package/lib/renderers/three/build/build.js.map +1 -1
  41. package/lib/renderers/three/compile/compile.d.ts +2 -0
  42. package/lib/renderers/three/compile/compile.d.ts.map +1 -1
  43. package/lib/renderers/three/compile/compile.js +6 -4
  44. package/lib/renderers/three/compile/compile.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/abstract-3d.test.ts +254 -0
  47. package/src/abstract-3d.ts +427 -85
  48. package/src/renderers/dxf/dxf-encoding/dxf-dimension.ts +22 -2
  49. package/src/renderers/dxf/dxf-geometries/dxf-dimension.ts +12 -2
  50. package/src/renderers/dxf/dxf.ts +21 -4
  51. package/src/renderers/ifc/ifc.ts +16 -3
  52. package/src/renderers/step/step.ts +3 -2
  53. package/src/renderers/stl/stl.ts +12 -4
  54. package/src/renderers/svg/svg-geometries/svg-text.ts +3 -2
  55. package/src/renderers/svg/svg.ts +29 -9
  56. package/src/renderers/svg-bsp/svg-geometries/svg-text.ts +3 -2
  57. package/src/renderers/svg-bsp/svg.ts +29 -9
  58. package/src/renderers/three/build/build.test.ts +2 -2
  59. package/src/renderers/three/build/build.ts +1 -1
  60. package/src/renderers/three/compile/compile.ts +22 -4
@@ -24,6 +24,26 @@ export type Dimensions = {
24
24
  readonly material: Material;
25
25
  };
26
26
 
27
+ /** Where a dimension's line and text are drawn, see `dimensionPlacements` */
28
+ export type DimensionPlacement = {
29
+ /** The dimension's `linePosition`, moved out where its line is too near the line inside it or the measured points for the texts between them */
30
+ readonly linePosition: Vec3;
31
+ readonly text: DimensionTextPlacement;
32
+ /**
33
+ * The lines at the dimension's ends: the marks across its dimension line and the extension lines from the measured points to them, broken around
34
+ * the texts they'd cross
35
+ */
36
+ readonly endLines: ReadonlyArray<readonly [Vec3, Vec3]>;
37
+ /** Everything the dimension draws: its dimension line, the lines at its ends and its text */
38
+ readonly bounds: Bounds3;
39
+ };
40
+
41
+ /**
42
+ * The text a gap from the dimension line, out from its middle, out from the line or, `inward`, towards the measured points; a text of several rows
43
+ * has its first row over the line, as the text reads, and the rest under it
44
+ */
45
+ export type DimensionTextPlacement = { readonly inward: boolean };
46
+
27
47
  export type Dimension = DimensionMesh | DimensionAligned;
28
48
 
29
49
  export type DimensionMesh = {
@@ -298,7 +318,19 @@ export type Text = {
298
318
  readonly pos: Vec3;
299
319
  readonly rot?: Vec3;
300
320
  readonly text: string;
301
- readonly fontSize: number;
321
+ /** The scene's text height, see `sceneTextHeight`, if not set */
322
+ readonly fontSize?: number;
323
+ readonly background?: TextBackground;
324
+ };
325
+
326
+ /** A circle or a rectangle `depth` behind the text, half as thick, `padding` past it on every side, its edge an `outline` wide band */
327
+ export type TextBackground = {
328
+ readonly shape: "circle" | "rectangle";
329
+ readonly padding: number;
330
+ readonly outline: number;
331
+ readonly depth: number;
332
+ readonly material: Material;
333
+ readonly outlineMaterial: Material;
302
334
  };
303
335
 
304
336
  export type Box = {
@@ -630,14 +662,16 @@ export function bounds3FromVec3Array(vec3Array: ReadonlyArray<Vec3>): Bounds3 {
630
662
  }
631
663
 
632
664
  export function boundsScene(scene: Scene): Bounds3 {
665
+ const textHeight = sceneTextHeight(scene.groups);
633
666
  const bounds: Array<Bounds3> = [];
634
667
  for (const group of scene.groups) {
635
- bounds.push(...boundsGroup(group, vec3Zero, vec3Zero));
668
+ bounds.push(...boundsGroup(group, vec3Zero, vec3Zero, textHeight));
636
669
  }
637
670
  return bounds3Merge(...bounds);
638
671
  }
639
672
 
640
- export function boundsGroup(group: Group, parentPos: Vec3, parentRot: Vec3): ReadonlyArray<Bounds3> {
673
+ /** The bounds of the group's meshes, its texts without a font size `textHeight` high */
674
+ export function boundsGroup(group: Group, parentPos: Vec3, parentRot: Vec3, textHeight: number = defaultDimensionTextHeight): ReadonlyArray<Bounds3> {
641
675
  const pos = vec3TransRot(group.pos, parentPos, parentRot);
642
676
  const rot = vec3RotCombine(parentRot, group.rot ?? vec3Zero);
643
677
  const bounds: Array<Bounds3> = [];
@@ -665,7 +699,7 @@ export function boundsGroup(group: Group, parentPos: Vec3, parentRot: Vec3): Rea
665
699
  break;
666
700
  }
667
701
  case "Text": {
668
- bounds.push(boundsText(childMesh.geometry, pos, rot));
702
+ bounds.push(boundsText(childMesh.geometry, pos, rot, textHeight));
669
703
  break;
670
704
  }
671
705
  default:
@@ -674,7 +708,7 @@ export function boundsGroup(group: Group, parentPos: Vec3, parentRot: Vec3): Rea
674
708
  }
675
709
 
676
710
  for (const childGroup of group.groups ?? []) {
677
- bounds.push(...boundsGroup(childGroup, pos, rot));
711
+ bounds.push(...boundsGroup(childGroup, pos, rot, textHeight));
678
712
  }
679
713
  return bounds;
680
714
  }
@@ -751,22 +785,83 @@ export function boundsCone(c: Cone, parentPos: Vec3, parentRot: Vec3): Bounds3 {
751
785
  return bounds3FromVec3Array(points);
752
786
  }
753
787
 
754
- export function boundsText(t: Text, parentPos: Vec3, parentRot: Vec3): Bounds3 {
755
- const characterRatio = 9.0 / 16.0;
756
- const charWidth = t.fontSize * characterRatio;
757
- const rows = t.text.split("\n");
758
- const width = Math.max(...rows.map((v) => v.length)) * charWidth;
759
- const height = rows.length * t.fontSize * 1.2;
760
- return boundsPlane(
761
- {
762
- type: "Plane",
763
- pos: t.pos,
764
- size: vec2(width, height),
765
- rot: t.rot,
766
- },
767
- parentPos,
768
- parentRot
769
- );
788
+ /** The bounds of the text and its background, `textHeight` high without a font size */
789
+ export function boundsText(t: Text, parentPos: Vec3, parentRot: Vec3, textHeight: number = defaultDimensionTextHeight): Bounds3 {
790
+ const size = textSize(t.text, t.fontSize ?? textHeight);
791
+ const padding = 2 * (t.background?.padding ?? 0);
792
+ return boundsPlane({ type: "Plane", pos: t.pos, size: vec2(size.x + padding, size.y + padding), rot: t.rot }, parentPos, parentRot);
793
+ }
794
+
795
+ /** How wide and high a text is drawn at the font size, its widest row by a rough character width and its rows a fifth of the font size apart */
796
+ export function textSize(text: string, fontSize: number): Vec2 {
797
+ const rows = text.split("\n");
798
+ return vec2(Math.max(...rows.map((v) => v.length)) * fontSize * (9 / 16), rows.length * fontSize * 1.2);
799
+ }
800
+
801
+ /** The longest side of the scene's objects that the text height 32 is for */
802
+ const textHeightBaseSize = 3600;
803
+ const textHeightBase = 32;
804
+ /** A step of the text height, half of ISO 3098's √2, so a scene just past a step has its text at most 19% larger than in proportion to its size */
805
+ const textHeightStep = 2 ** (1 / 4);
806
+
807
+ /**
808
+ * The text height for a scene whose objects take `size`, without its dimensions and texts: 32 up to the base size, else as many steps of 2^¼ above it
809
+ * as the longest side needs, rounded up, so a text is never smaller on the drawing than 32 on the base size
810
+ */
811
+ export function textHeightForSize(size: Vec3): number {
812
+ const steps = Math.ceil(Math.log(Math.max(size.x, size.y, size.z) / textHeightBaseSize) / Math.log(textHeightStep) - 1e-9);
813
+ return textHeightBase * textHeightStep ** Math.max(0, steps);
814
+ }
815
+
816
+ /**
817
+ * The text height of a scene of the groups, its texts without a font size and its dimensions': `textHeightForSize` of its objects' size, without its
818
+ * texts, which the text height would size
819
+ */
820
+ export function sceneTextHeight(groups: ReadonlyArray<Group>): number {
821
+ const withoutTexts = (g: Group): Group => ({
822
+ ...g,
823
+ meshes: g.meshes?.filter((m) => m.geometry.type !== "Text"),
824
+ groups: g.groups?.map(withoutTexts),
825
+ });
826
+ return textHeightForSize(bounds3ToSize(bounds3Merge(...groups.flatMap((g) => boundsGroup(withoutTexts(g), vec3Zero, vec3Zero)))));
827
+ }
828
+
829
+ /** The group with its texts' backgrounds as the meshes just behind each text, in a group after its own, sized by its font size or else `textHeight` */
830
+ export function groupTextBackgrounds(g: Group, textHeight: number): Group {
831
+ const backgrounds = (g.meshes ?? []).flatMap((m): ReadonlyArray<Group> => {
832
+ if (m.geometry.type !== "Text" || !m.geometry.background) {
833
+ return [];
834
+ }
835
+ const { padding, outline, depth, material, outlineMaterial, shape } = m.geometry.background;
836
+ const size = textSize(m.geometry.text, m.geometry.fontSize ?? textHeight);
837
+ const width = size.x + 2 * padding;
838
+ const height = size.y + 2 * padding;
839
+ const thickness = depth / 2;
840
+ const back = vec3(0, 0, -depth);
841
+ const radius = Math.max(width, height) / 2;
842
+ return [
843
+ group(
844
+ shape === "circle"
845
+ ? [
846
+ cylinder(back, radius - outline, thickness, material, vec3(Math.PI / 2, 0, 0)),
847
+ cylinder(back, radius, thickness, outlineMaterial, vec3(Math.PI / 2, 0, 0), false, [
848
+ { type: "RoundHole", radius: radius - outline, pos: vec2Zero },
849
+ ]),
850
+ ]
851
+ : [
852
+ plane(back, vec2(width - 2 * outline, height - 2 * outline), material),
853
+ box(vec3(width, outline, thickness), outlineMaterial, vec3Add(back, vec3(0, (height - outline) / 2, 0))),
854
+ box(vec3(width, outline, thickness), outlineMaterial, vec3Add(back, vec3(0, -(height - outline) / 2, 0))),
855
+ box(vec3(outline, height - 2 * outline, thickness), outlineMaterial, vec3Add(back, vec3((width - outline) / 2, 0, 0))),
856
+ box(vec3(outline, height - 2 * outline, thickness), outlineMaterial, vec3Add(back, vec3(-(width - outline) / 2, 0, 0))),
857
+ ],
858
+ m.geometry.pos,
859
+ m.geometry.rot ?? vec3Zero
860
+ ),
861
+ ];
862
+ });
863
+ const groups = (g.groups ?? []).map((c) => groupTextBackgrounds(c, textHeight));
864
+ return backgrounds.length === 0 && g.groups === undefined ? g : { ...g, groups: [...groups, ...backgrounds] };
770
865
  }
771
866
 
772
867
  // -- Transformations
@@ -1099,8 +1194,15 @@ export const culledLine = (start: Vec3, end: Vec3, normal: Vec3, thickness: numb
1099
1194
  material,
1100
1195
  });
1101
1196
 
1102
- export const text = (pos: Vec3, text: string, fontSize: number, material: Material, rot = vec3Zero): TextMesh => ({
1103
- geometry: { type: "Text", pos, text, fontSize, rot },
1197
+ export const text = (
1198
+ pos: Vec3,
1199
+ text: string,
1200
+ fontSize: number | undefined,
1201
+ material: Material,
1202
+ rot = vec3Zero,
1203
+ background?: TextBackground
1204
+ ): TextMesh => ({
1205
+ geometry: { type: "Text", pos, text, fontSize, rot, background },
1104
1206
  material,
1105
1207
  });
1106
1208
 
@@ -1143,6 +1245,236 @@ export function dimensionIsOfTypeMesh(dimension: Dimension): dimension is Dimens
1143
1245
  return "meshes" in dimension;
1144
1246
  }
1145
1247
 
1248
+ const defaultDimensionTextHeight = 44;
1249
+ // The sizes as shares of the text height
1250
+ const dimensionGap = 8 / defaultDimensionTextHeight;
1251
+ /** How far the marks at a dimension's ends go to each side of the dimension line, short of the text's digits a gap from it */
1252
+ const dimensionMark = 10 / defaultDimensionTextHeight;
1253
+ const dimensionArrowLength = 22.5 / defaultDimensionTextHeight;
1254
+ const dimensionArrowHalfBase = 5.625 / defaultDimensionTextHeight;
1255
+ /** How far apart the rows of a dimension's text are, as `textSize` measures them, each drawn as a text of its own so every renderer spaces them alike */
1256
+ const dimensionTextRowStep = 1.2;
1257
+ /** The lines' thickness, whatever the text height, as the three renderer draws it in pixels */
1258
+ const dimensionLineThickness = 0.5;
1259
+
1260
+ /** A dimension's measured length, its text and how wide the text is drawn, as `textSize` measures it */
1261
+ function dimensionMeasure(
1262
+ dimension: DimensionAligned,
1263
+ textHeight: number
1264
+ ): { readonly length: number; readonly text: string; readonly rows: ReadonlyArray<string>; readonly textWidth: number } {
1265
+ const length = vec3Length(vec3Sub(dimension.measurementEnd, dimension.measurementStart));
1266
+ const text = dimension.text ?? length.toFixed(1).replace(/[.,]0$/, "");
1267
+ const rows = text.split("\n");
1268
+ return { length, text, rows, textWidth: textSize(text, textHeight).x };
1269
+ }
1270
+
1271
+ type DimensionSpan = {
1272
+ readonly index: number;
1273
+ readonly view: View;
1274
+ /** The direction along the line, the same for the dimensions measured either way along it */
1275
+ readonly along: Vec3;
1276
+ /** Where the dimension line crosses the plane through the origin across it, the same for the dimensions on one line */
1277
+ readonly linePoint: Vec3;
1278
+ readonly linePosition: Vec3;
1279
+ readonly measurementStart: Vec3;
1280
+ readonly measurementEnd: Vec3;
1281
+ /** The direction from the measured points out to the line */
1282
+ readonly outward: Vec3;
1283
+ /** How far out the line is */
1284
+ readonly row: number;
1285
+ /** How far out the outermost measured point is */
1286
+ readonly measured: number;
1287
+ readonly min: number;
1288
+ readonly max: number;
1289
+ readonly startIsMin: boolean;
1290
+ readonly textWidth: number;
1291
+ /** A text of several rows, on both sides of the line */
1292
+ readonly split: boolean;
1293
+ /** How far the text takes out from each side of the line it's on */
1294
+ readonly textBlockHeight: number;
1295
+ };
1296
+
1297
+ /**
1298
+ * Where each dimension's line and text are drawn, decided for the dimensions on the same line together, undefined for a mesh dimension:
1299
+ *
1300
+ * - The text a gap out from the line, out from the dimension's middle, wider than the dimension where nothing's in the way
1301
+ * - Else on the other side of the line, towards the measured points, alternating with its neighbours'
1302
+ * - Placed together for the dimensions on a line, whichever together are the clearest: texts out from the line, and apart
1303
+ * - Each line moved out where it's too near the measured points or the line inside it on the same side for the texts between them
1304
+ */
1305
+ export function dimensionPlacements(
1306
+ dimensions: ReadonlyArray<Dimension>,
1307
+ textHeight: number = defaultDimensionTextHeight
1308
+ ): ReadonlyArray<DimensionPlacement | undefined> {
1309
+ const tolerance = textHeight * 0.01;
1310
+ const gap = textHeight * dimensionGap;
1311
+ const lines = Array<Array<DimensionSpan>>();
1312
+ dimensions.forEach((d, index) => {
1313
+ if (dimensionIsOfTypeMesh(d)) {
1314
+ return;
1315
+ }
1316
+ const direction = vec3Normalize(vec3Sub(d.measurementEnd, d.measurementStart));
1317
+ const flip = Math.abs(direction.x) > 1e-6 ? direction.x < 0 : Math.abs(direction.y) > 1e-6 ? direction.y < 0 : direction.z < 0;
1318
+ const along = flip ? vec3Scale(direction, -1) : direction;
1319
+ const start = vec3Dot(d.measurementStart, along);
1320
+ const end = vec3Dot(d.measurementEnd, along);
1321
+ const linePoint = vec3Sub(d.linePosition, vec3Scale(along, vec3Dot(d.linePosition, along)));
1322
+ const outward = vec3Normalize(vec3Sub(linePoint, vec3Sub(d.measurementStart, vec3Scale(along, start))));
1323
+ const { textWidth, rows } = dimensionMeasure(d, textHeight);
1324
+ const span: DimensionSpan = {
1325
+ index,
1326
+ view: d.views?.[0] ?? "front",
1327
+ along,
1328
+ linePoint,
1329
+ linePosition: d.linePosition,
1330
+ measurementStart: d.measurementStart,
1331
+ measurementEnd: d.measurementEnd,
1332
+ outward,
1333
+ row: vec3Dot(d.linePosition, outward),
1334
+ measured: Math.max(vec3Dot(d.measurementStart, outward), vec3Dot(d.measurementEnd, outward)),
1335
+ min: Math.min(start, end),
1336
+ max: Math.max(start, end),
1337
+ startIsMin: start <= end,
1338
+ textWidth,
1339
+ split: rows.length > 1,
1340
+ // Its first row on one side, the rest on the other
1341
+ textBlockHeight: textHeight * (1 + dimensionTextRowStep * Math.max(0, rows.length - 2)),
1342
+ };
1343
+ const line = lines.find(
1344
+ ([s]) => s && s.view === span.view && vec3Dot(s.along, along) > 1 - 1e-6 && vec3Length(vec3Sub(s.linePoint, span.linePoint)) < tolerance
1345
+ );
1346
+ if (line) {
1347
+ line.push(span);
1348
+ } else {
1349
+ lines.push([span]);
1350
+ }
1351
+ });
1352
+
1353
+ const sameSide = (a: DimensionSpan | undefined, b: DimensionSpan): boolean =>
1354
+ a !== undefined && a.view === b.view && vec3Dot(a.along, b.along) > 1 - 1e-6 && vec3Dot(a.outward, b.outward) > 1 - 1e-6;
1355
+ // The lines' texts, placed before the lines are moved out, as the room on each side of a line is left for its texts
1356
+ const decisions = lines.flatMap((line) => {
1357
+ const [first] = line;
1358
+ if (!first) {
1359
+ return [];
1360
+ }
1361
+ type Placed = { readonly s: DimensionSpan; readonly inward: boolean };
1362
+ // How far apart two texts out from their dimensions' middles are
1363
+ const apart = (l: Placed, o: Placed): number => Math.abs((l.s.min + l.s.max) / 2 - (o.s.min + o.s.max) / 2) - (l.s.textWidth + o.s.textWidth) / 2;
1364
+ const sorted = line.toSorted((a, b) => a.min - b.min);
1365
+ // What makes a text unclear, worst first: across its neighbour's, so they alternate even where every other one's still clash, across another text,
1366
+ // two texts on one side so close they read as one, and a text towards the measured points, more at an end of the line, with room past it
1367
+ const placeCost = (l: Placed, placed: ReadonlyArray<Placed>): number =>
1368
+ (l.inward ? (l.s === sorted[0] || l.s === sorted.at(-1) ? 5 : 4) : 0) +
1369
+ placed
1370
+ .filter((o) => o.inward === l.inward || o.s.split || l.s.split)
1371
+ .reduce(
1372
+ (sum, o) =>
1373
+ sum +
1374
+ (apart(l, o) < 2 * gap - tolerance
1375
+ ? o === placed.at(-1)
1376
+ ? 100000
1377
+ : 1000
1378
+ : apart(l, o) < textHeight / 2
1379
+ ? 10 * (1 - (2 * apart(l, o)) / textHeight)
1380
+ : 0),
1381
+ 0
1382
+ );
1383
+ const placed =
1384
+ sorted
1385
+ .reduce<ReadonlyArray<{ readonly placed: ReadonlyArray<Placed>; readonly cost: number }>>(
1386
+ (states, s) =>
1387
+ states
1388
+ .flatMap(({ placed, cost }) =>
1389
+ (s.split ? [false] : [false, true]).map((inward) => ({
1390
+ placed: [...placed, { s, inward }],
1391
+ cost: cost + placeCost({ s, inward }, placed),
1392
+ }))
1393
+ )
1394
+ .toSorted((a, b) => a.cost - b.cost)
1395
+ .slice(0, 64),
1396
+ [{ placed: [], cost: 0 }]
1397
+ )
1398
+ .at(0)?.placed ?? [];
1399
+ return [{ line, first, placed }];
1400
+ });
1401
+
1402
+ // How far a line's texts, or the marks at its ends, take on each side of it
1403
+ const mark = textHeight * dimensionMark;
1404
+ const reach = (placed: ReadonlyArray<{ readonly s: DimensionSpan; readonly inward: boolean }>, inward: boolean): number =>
1405
+ Math.max(mark, ...placed.filter((l) => l.inward === inward || l.s.split).map((l) => gap + l.s.textBlockHeight));
1406
+ // How far out each line is moved to: its texts towards the measured points a gap past them or past the texts out from the line inside it on the same
1407
+ // side
1408
+ const rows = new Map<DimensionSpan, number>();
1409
+ for (const { line, first, placed } of decisions.toSorted((a, b) => a.first.row - b.first.row)) {
1410
+ rows.set(
1411
+ first,
1412
+ Math.max(
1413
+ first.row,
1414
+ Math.max(...line.map((s) => s.measured)) + gap + reach(placed, true),
1415
+ ...decisions
1416
+ .filter((d) => sameSide(d.first, first) && d.first.row < first.row - tolerance)
1417
+ .map((d) => (rows.get(d.first) ?? 0) + reach(d.placed, false) + gap + reach(placed, true))
1418
+ )
1419
+ );
1420
+ }
1421
+
1422
+ // Every text's rectangle on each side it's on, along its line from its left to its right and out from its bottom to its top
1423
+ const texts = decisions.flatMap(({ first, placed }) =>
1424
+ placed.flatMap(({ s, inward }) =>
1425
+ (s.split ? [false, true] : [inward]).map((side) => {
1426
+ const row = rows.get(first) ?? first.row;
1427
+ const middle = (s.min + s.max) / 2;
1428
+ return {
1429
+ s,
1430
+ left: middle - s.textWidth / 2,
1431
+ right: middle + s.textWidth / 2,
1432
+ bottom: side ? row - gap - s.textBlockHeight : row + gap,
1433
+ top: side ? row - gap : row + gap + s.textBlockHeight,
1434
+ };
1435
+ })
1436
+ )
1437
+ );
1438
+ const placements = new Map<number, DimensionPlacement>();
1439
+ for (const { first, placed } of decisions) {
1440
+ const row = rows.get(first) ?? first.row;
1441
+ for (const { s, inward } of placed) {
1442
+ const linePoint = vec3Add(s.linePoint, vec3Scale(s.outward, row - s.row));
1443
+ const at = (t: number, u: number): Vec3 => vec3Add(linePoint, vec3Add(vec3Scale(s.along, t), vec3Scale(s.outward, u - row)));
1444
+ // At each end the mark across the line, and the extension line from the measured point to it, broken half a gap short of each text
1445
+ // it'd cross on the same side
1446
+ const endLines = [
1447
+ [s.min, s.startIsMin ? s.measurementStart : s.measurementEnd] as const,
1448
+ [s.max, s.startIsMin ? s.measurementEnd : s.measurementStart] as const,
1449
+ ].flatMap(([t, point]) => {
1450
+ const extension = texts
1451
+ .filter((o) => sameSide(o.s, s) && t > o.left && t < o.right)
1452
+ .reduce<ReadonlyArray<readonly [number, number]>>(
1453
+ (pieces, o) =>
1454
+ pieces.flatMap(([from, to]) => [[from, Math.min(to, o.bottom - gap / 2)] as const, [Math.max(from, o.top + gap / 2), to] as const]),
1455
+ [[vec3Dot(point, s.outward), row - mark]]
1456
+ )
1457
+ .filter(([from, to]) => to - from >= gap);
1458
+ return [...extension, [row - mark, row + mark] as const].map(([from, to]) => [at(t, from), at(t, to)] as const);
1459
+ });
1460
+ // The rectangle the dimension line, the marks at its ends and its text take, and the extension lines
1461
+ const middle = (s.min + s.max) / 2;
1462
+ const corners = [Math.min(s.min, middle - s.textWidth / 2), Math.max(s.max, middle + s.textWidth / 2)].flatMap((t) =>
1463
+ [inward || s.split ? -gap - s.textBlockHeight : -mark, inward && !s.split ? mark : gap + s.textBlockHeight].map((u) => at(t, row + u))
1464
+ );
1465
+ const linePosition = vec3Add(s.linePosition, vec3Scale(s.outward, row - s.row));
1466
+ placements.set(s.index, {
1467
+ linePosition,
1468
+ text: { inward },
1469
+ endLines,
1470
+ bounds: bounds3FromVec3Array([...corners, ...endLines.flat()]),
1471
+ });
1472
+ }
1473
+ }
1474
+ return dimensions.map((_, i) => placements.get(i));
1475
+ }
1476
+
1477
+ /** Draws the dimension as `placement` places it, its text `textHeight` high; as if alone on its line if not given */
1146
1478
  export function dimensionMeshifyAlignedDimension(
1147
1479
  dimension: DimensionAligned,
1148
1480
  sceneRotation: Vec3,
@@ -1150,25 +1482,23 @@ export function dimensionMeshifyAlignedDimension(
1150
1482
  onCreateLine: (start: Vec3, end: Vec3, norm: Vec3, thickness: number, mat: Material) => void,
1151
1483
  onCreateText: (pos: Vec3, measurement: string, fontSize: number, mat: Material, rot: Vec3, dir: Vec3, normal: Vec3) => void,
1152
1484
  onCreatePolygon: (p1: Vec3, p2: Vec3, p3: Vec3, mat: Material) => void,
1153
- dimensionsMaterial?: Material
1485
+ dimensionsMaterial?: Material,
1486
+ textHeight: number = defaultDimensionTextHeight,
1487
+ placement?: DimensionPlacement
1154
1488
  ): void {
1155
1489
  const defaultMaterial = { normal: "rgb(0, 0, 0)", opacity: 1.0, roughness: 1.0, metalness: 0.0 };
1156
1490
  const material = dimension.material ?? dimensionsMaterial ?? defaultMaterial;
1157
- const fontGlyphWidthRatio = 0.4815;
1158
- const textSize = 44;
1159
- const textOffset = 8;
1160
- const textThreshold = textOffset + textSize * fontGlyphWidthRatio * 7;
1161
- const lineThickness = 0.5;
1162
- const arrowLength = 40;
1163
- const arrowHalfBase = 10;
1164
-
1165
- const lp = dimension.linePosition;
1491
+ const place = placement ?? dimensionPlacements([dimension], textHeight)[0];
1492
+ if (!place) {
1493
+ return;
1494
+ }
1495
+ const gap = textHeight * dimensionGap;
1496
+ const lineThickness = dimensionLineThickness;
1497
+
1498
+ const lp = place.linePosition;
1166
1499
  const ms = dimension.measurementStart;
1167
1500
  const me = dimension.measurementEnd;
1168
- const measurementLength = vec3Length(vec3Sub(me, ms));
1169
- const measurement = dimension.text ?? measurementLength.toFixed(1).replace(/[.,]0$/, "");
1170
- const measurementRows = measurement.split("\n");
1171
- const textHeight = measurementRows.length * textSize;
1501
+ const { rows: measurementRows } = dimensionMeasure(dimension, textHeight);
1172
1502
 
1173
1503
  const view = dimension.views?.[0] ?? "front";
1174
1504
  const basis = dimensionViewBasises[view];
@@ -1181,17 +1511,32 @@ export function dimensionMeshifyAlignedDimension(
1181
1511
  const offset = vec3Sub(lp, projected);
1182
1512
  const ls = vec3Add(ms, offset);
1183
1513
  const le = vec3Add(me, offset);
1184
- const linesDiff = vec3Sub(ls, ms);
1185
- const dirBetweenLines = vec3Normalize(linesDiff);
1186
- const measurementWidth = vec3Length(linesDiff);
1187
- const displacedDistance = measurementWidth - textHeight / 2;
1188
- const offsetDisplaced = vec3Scale(offset, displacedDistance / measurementWidth);
1189
- const lsDisplaced = vec3Add(ms, offsetDisplaced);
1190
- const leDisplaced = vec3Add(me, offsetDisplaced);
1191
- const lcDisplaced = vec3Scale(vec3Add(lsDisplaced, leDisplaced), 0.5);
1192
-
1193
- onCreateLine(ms, ls, basis.normal, lineThickness, material);
1194
- onCreateLine(me, le, basis.normal, lineThickness, material);
1514
+ const lc = vec3Scale(vec3Add(ls, le), 0.5);
1515
+ const dirBetweenLines = vec3Normalize(offset);
1516
+
1517
+ // The lines at its ends and the dimension line
1518
+ place.endLines.forEach(([start, end]) => onCreateLine(start, end, basis.normal, lineThickness, material));
1519
+ onCreateLine(ls, le, basis.normal, lineThickness, material);
1520
+
1521
+ // An arrow inside each end, pointing at its mark, where both fit with a gap of the line between them
1522
+ const arrowLength = textHeight * dimensionArrowLength;
1523
+ const arrowHalfBase = textHeight * dimensionArrowHalfBase;
1524
+ if (vec3Length(vec3Sub(le, ls)) >= 2 * arrowLength + gap) {
1525
+ for (const [tip, inward] of [
1526
+ [ls, direction],
1527
+ [le, vec3Scale(direction, -1)],
1528
+ ] as const) {
1529
+ const base = vec3Add(tip, vec3Scale(inward, arrowLength));
1530
+ const a = vec3Add(base, vec3Scale(dirBetweenLines, arrowHalfBase));
1531
+ const b = vec3Sub(base, vec3Scale(dirBetweenLines, arrowHalfBase));
1532
+ // The winding order facing the view's normal
1533
+ if (vec3Dot(vec3Cross(vec3Sub(a, tip), vec3Sub(b, tip)), basis.normal) >= 0) {
1534
+ onCreatePolygon(tip, a, b, material);
1535
+ } else {
1536
+ onCreatePolygon(tip, b, a, material);
1537
+ }
1538
+ }
1539
+ }
1195
1540
 
1196
1541
  const rotatedDirBetweenLines = vec3TransRot(dirBetweenLines, vec3Zero, sceneRotation);
1197
1542
  const rotatedNormal = vec3TransRot(basis.normal, vec3Zero, sceneRotation);
@@ -1205,44 +1550,31 @@ export function dimensionMeshifyAlignedDimension(
1205
1550
  const finalBasis = dimensionViewBasises.front;
1206
1551
  const finalSide = getDimensionSide(dirBetweenLines, finalBasis);
1207
1552
  const finalDir = getDimensionTextDirection(finalSide, finalBasis);
1208
- onCreateText(lcDisplaced, measurement, textSize, material, textRot, finalDir, vec3(0, 0, 1));
1209
-
1210
- if (measurementLength > textThreshold) {
1211
- const textWidth = Math.max(...measurementRows.map((t) => t.length)) * (textSize * fontGlyphWidthRatio);
1212
- const lineDir = vec3Normalize(vec3Sub(leDisplaced, lsDisplaced));
1213
- const halfTextWidth = textOffset + textWidth * 0.5;
1214
- const leftEnd = vec3Sub(lcDisplaced, vec3Scale(lineDir, halfTextWidth));
1215
- const rightStart = vec3Add(lcDisplaced, vec3Scale(lineDir, halfTextWidth));
1216
-
1217
- onCreateLine(lsDisplaced, leftEnd, basis.normal, lineThickness, material);
1218
- onCreateLine(rightStart, leDisplaced, basis.normal, lineThickness, material);
1219
-
1220
- //arrows
1221
- const arrowBaseStart = vec3Add(lsDisplaced, vec3Scale(lineDir, arrowLength));
1222
- const arrowBaseEnd = vec3Add(leDisplaced, vec3Scale(lineDir, -arrowLength));
1223
- const arrowBaseStart1 = vec3Add(arrowBaseStart, vec3Scale(dirBetweenLines, arrowHalfBase));
1224
- const arrowBaseStart2 = vec3Add(arrowBaseStart, vec3Scale(dirBetweenLines, -arrowHalfBase));
1225
- const arrowBaseEnd1 = vec3Add(arrowBaseEnd, vec3Scale(dirBetweenLines, arrowHalfBase));
1226
- const arrowBaseEnd2 = vec3Add(arrowBaseEnd, vec3Scale(dirBetweenLines, -arrowHalfBase));
1227
-
1228
- //the winding order needs to be correct according to the normal
1229
- const polygonFacing = (a: Vec3, b: Vec3, c: Vec3): void => {
1230
- const ab = vec3Sub(b, a);
1231
- const ac = vec3Sub(c, a);
1232
- const triangleNormal = vec3Cross(ab, ac);
1233
-
1234
- if (vec3Dot(triangleNormal, basis.normal) >= 0) {
1235
- onCreatePolygon(a, b, c, material);
1236
- } else {
1237
- onCreatePolygon(a, c, b, material);
1238
- }
1239
- };
1240
- polygonFacing(lsDisplaced, arrowBaseStart2, arrowBaseStart1);
1241
- polygonFacing(leDisplaced, arrowBaseEnd1, arrowBaseEnd2);
1242
- }
1553
+ // Each row a text of its own: one row a gap out from the line or towards the measured points, of several the first a gap over the line, as the
1554
+ // text reads, and the rest from a gap under it
1555
+ const textUp = vec3Normalize(vec3Cross(basis.normal, textDir));
1556
+ measurementRows.forEach((row, i) =>
1557
+ onCreateText(
1558
+ measurementRows.length === 1
1559
+ ? vec3Add(lc, vec3Scale(dirBetweenLines, (place.text.inward ? -1 : 1) * (gap + textHeight / 2)))
1560
+ : vec3Add(lc, vec3Scale(textUp, i === 0 ? gap + textHeight / 2 : -(gap + textHeight / 2 + (i - 1) * dimensionTextRowStep * textHeight))),
1561
+ row,
1562
+ textHeight,
1563
+ material,
1564
+ textRot,
1565
+ finalDir,
1566
+ vec3(0, 0, 1)
1567
+ )
1568
+ );
1243
1569
  }
1244
1570
 
1245
- export function dimensionConvertToTypeMesh(dimension: Dimension, sceneRotation: Vec3, dimensionsMaterial?: Material): DimensionMesh {
1571
+ export function dimensionConvertToTypeMesh(
1572
+ dimension: Dimension,
1573
+ sceneRotation: Vec3,
1574
+ dimensionsMaterial?: Material,
1575
+ textHeight?: number,
1576
+ placement?: DimensionPlacement
1577
+ ): DimensionMesh {
1246
1578
  if (dimensionIsOfTypeMesh(dimension)) {
1247
1579
  return dimension;
1248
1580
  }
@@ -1257,7 +1589,17 @@ export function dimensionConvertToTypeMesh(dimension: Dimension, sceneRotation:
1257
1589
  const onCreatePolygon = (p1: Vec3, p2: Vec3, p3: Vec3, mat: Material): void => {
1258
1590
  meshes.push(polygon([p1, p2, p3], mat));
1259
1591
  };
1260
- dimensionMeshifyAlignedDimension(dimension, sceneRotation, vec3Zero, onCreateLine, onCreateText, onCreatePolygon, dimensionsMaterial);
1592
+ dimensionMeshifyAlignedDimension(
1593
+ dimension,
1594
+ sceneRotation,
1595
+ vec3Zero,
1596
+ onCreateLine,
1597
+ onCreateText,
1598
+ onCreatePolygon,
1599
+ dimensionsMaterial,
1600
+ textHeight,
1601
+ placement
1602
+ );
1261
1603
 
1262
1604
  return {
1263
1605
  meshes,
@@ -1,5 +1,5 @@
1
1
  import { dimensionMeshifyAlignedDimension } from "../../../abstract-3d.js";
2
- import type { DimensionAligned, View, Material, Vec3 } from "../../../abstract-3d.js";
2
+ import type { DimensionAligned, DimensionPlacement, View, Material, Vec3 } from "../../../abstract-3d.js";
3
3
  import { type DxfColor, dxfColor } from "./dxf-color.js";
4
4
  import { DXF_BLOCK_RECORD_TABLE_HANDLE, DXF_MODEL_SPACE_HANDLE, dxfHandleNext, dxfRound, type Handle } from "./dxf-common.js";
5
5
  import { dxfEncLine } from "./dxf-line.js";
@@ -21,6 +21,8 @@ export function dxfEncDimension(
21
21
  sceneRotation: Vec3,
22
22
  viewRotation: Vec3,
23
23
  views: ReadonlyArray<View>,
24
+ textHeight: number | undefined,
25
+ placement: DimensionPlacement | undefined,
24
26
  handleRef: Handle
25
27
  ): DxfDimensionDefinition {
26
28
  const blockRecordHandle = dxfHandleNext(handleRef);
@@ -38,6 +40,8 @@ export function dxfEncDimension(
38
40
  sceneRotation,
39
41
  viewRotation,
40
42
  views,
43
+ textHeight,
44
+ placement,
41
45
  handleRef
42
46
  ),
43
47
  blockRecord: dxfEncodeDimensionBlockRecord(blockName, blockRecordHandle),
@@ -128,6 +132,8 @@ function dxfEncodeDimensionBlock(
128
132
  sceneRotation: Vec3,
129
133
  viewRotation: Vec3,
130
134
  views: ReadonlyArray<View>,
135
+ textHeight: number | undefined,
136
+ placement: DimensionPlacement | undefined,
131
137
  handleRef: Handle
132
138
  ): string {
133
139
  return `0
@@ -166,6 +172,8 @@ ${dxfEncodeDimensionGeometry(
166
172
  sceneRotation,
167
173
  viewRotation,
168
174
  views,
175
+ textHeight,
176
+ placement,
169
177
  handleRef
170
178
  )}0
171
179
  ENDBLK
@@ -194,6 +202,8 @@ function dxfEncodeDimensionGeometry(
194
202
  sceneRotation: Vec3,
195
203
  viewRotation: Vec3,
196
204
  views: ReadonlyArray<View>,
205
+ textHeight: number | undefined,
206
+ placement: DimensionPlacement | undefined,
197
207
  handleRef: Handle
198
208
  ): string {
199
209
  let entities = "";
@@ -214,6 +224,16 @@ function dxfEncodeDimensionGeometry(
214
224
  text,
215
225
  views,
216
226
  };
217
- dimensionMeshifyAlignedDimension(dimension, sceneRotation, viewRotation, onCreateLine, onCreateText, onCreatePolygon);
227
+ dimensionMeshifyAlignedDimension(
228
+ dimension,
229
+ sceneRotation,
230
+ viewRotation,
231
+ onCreateLine,
232
+ onCreateText,
233
+ onCreatePolygon,
234
+ undefined,
235
+ textHeight,
236
+ placement
237
+ );
218
238
  return entities;
219
239
  }