pa_font 0.2.3 → 0.2.4

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/USAGE.md CHANGED
@@ -189,6 +189,10 @@ const shape = paragraph.toShape({
189
189
 
190
190
  문단 전체를 plain region 데이터로 반환합니다.
191
191
 
192
+ ### `paragraph.toRegionViews(options?)`
193
+
194
+ 문단 전체를 zero-copy region view로 반환합니다.
195
+
192
196
  ### `paragraph.toPoints(options?)`
193
197
 
194
198
  문단 전체를 점 샘플로 반환합니다.
@@ -234,6 +238,11 @@ const shape = paragraph.toShape({
234
238
 
235
239
  최종 polygon 데이터를 반환합니다.
236
240
 
241
+ ### `shape.toRegionViews({ step, openWidth })`
242
+
243
+ 내부 polygon을 복사하지 않는 read-only view를 반환합니다.
244
+ 반복 후처리나 시각화처럼 바로 순회만 할 때 `toRegions()`보다 가볍습니다.
245
+
237
246
  반환 형태:
238
247
 
239
248
  ```js
package/dist/paFont.cjs CHANGED
@@ -560,13 +560,14 @@ function toArrayBuffer(value) {
560
560
  //#endregion
561
561
  //#region src/paFont/shape.js
562
562
  var PAShape = class {
563
- constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon }) {
563
+ constructor({ text, parts, contours, glyphs, bbox, metrics, edgeEpsilon, rawData }) {
564
564
  this.text = text;
565
565
  this.parts = parts;
566
566
  this.bbox = bbox;
567
567
  this.metrics = metrics;
568
568
  this.edgeEpsilon = edgeEpsilon;
569
569
  this.raw = {
570
+ ...rawData ?? {},
570
571
  contours,
571
572
  glyphs
572
573
  };
@@ -599,12 +600,23 @@ var PAShape = class {
599
600
  toRegions(options = {}) {
600
601
  return createRegionCollection(this.toShape(options));
601
602
  }
603
+ toRegionViews(options = {}) {
604
+ return this.toShape(options)._getRegionViews();
605
+ }
602
606
  openHoles(width) {
603
607
  const slitWidth = normalizePositive(width, 0);
604
608
  if (slitWidth <= 0 || !this.parts.some((part) => part.holes.length > 0)) return this;
605
609
  const cacheKey = toCacheKey(slitWidth);
606
610
  const cached = this._cache.openHoles.get(cacheKey);
607
611
  if (cached) return cached;
612
+ const glyphVariant = createGlyphDerivedShape(this, {
613
+ openWidth: slitWidth,
614
+ step: 0
615
+ });
616
+ if (glyphVariant) {
617
+ this._cache.openHoles.set(cacheKey, glyphVariant);
618
+ return glyphVariant;
619
+ }
608
620
  const geometryEpsilon = resolveGeometryEpsilon(slitWidth);
609
621
  const shape = createDerivedShape(this, this.parts.map((part) => openPartWithSlit(part, slitWidth, geometryEpsilon)));
610
622
  this._cache.openHoles.set(cacheKey, shape);
@@ -616,6 +628,14 @@ var PAShape = class {
616
628
  const cacheKey = toCacheKey(spacing);
617
629
  const cached = this._cache.resample.get(cacheKey);
618
630
  if (cached) return cached;
631
+ const glyphVariant = createGlyphDerivedShape(this, {
632
+ openWidth: 0,
633
+ step: spacing
634
+ });
635
+ if (glyphVariant) {
636
+ this._cache.resample.set(cacheKey, glyphVariant);
637
+ return glyphVariant;
638
+ }
619
639
  const shape = createDerivedShape(this, this.parts.map((part) => resamplePart(part, spacing)));
620
640
  this._cache.resample.set(cacheKey, shape);
621
641
  return shape;
@@ -677,8 +697,13 @@ function createTextShape(layout, opts, fontInstance) {
677
697
  const parts = [];
678
698
  const contours = [];
679
699
  const glyphs = [];
700
+ const sourceLayoutGlyphs = Array.isArray(layout.glyphs) ? layout.glyphs.slice() : [];
701
+ const variantOptions = {
702
+ openWidth: normalizePositive(opts.openWidth, 0),
703
+ step: normalizePositive(opts.step, 0)
704
+ };
680
705
  layout.glyphs.forEach((item, glyphPosition) => {
681
- const translated = translateGlyphGeometry(fontInstance._getFlattenedGlyph(item.glyph, opts), item.x, item.y, glyphPosition);
706
+ const translated = translateGlyphGeometry(fontInstance._getGlyphGeometryVariant(item.glyph, opts), item.x, item.y, glyphPosition);
682
707
  parts.push(...translated.parts);
683
708
  contours.push(...translated.contours);
684
709
  glyphs.push({
@@ -702,11 +727,21 @@ function createTextShape(layout, opts, fontInstance) {
702
727
  ...layout.metrics,
703
728
  bbox
704
729
  },
705
- edgeEpsilon: opts.edgeEpsilon
730
+ edgeEpsilon: opts.edgeEpsilon,
731
+ rawData: {
732
+ fontInstance,
733
+ sourceLayout: {
734
+ glyphs: sourceLayoutGlyphs,
735
+ metrics: { ...layout.metrics }
736
+ },
737
+ sourceOptions: extractShapeSourceOptions(opts),
738
+ variantOptions
739
+ }
706
740
  });
707
741
  }
708
742
  function createGlyphShape(shape, glyphPosition) {
709
743
  const glyphMeta = shape.raw.glyphs?.[glyphPosition] ?? null;
744
+ const sourceGlyph = shape.raw.sourceLayout?.glyphs?.[glyphPosition] ?? null;
710
745
  const parts = copyParts(shape.parts.filter((part) => part.glyphPosition === glyphPosition));
711
746
  const contours = copyContours((shape.raw.contours ?? []).filter((contour) => contour.glyphPosition === glyphPosition));
712
747
  const bbox = combineRects(parts.map((part) => part.bbox)) ?? (glyphMeta?.bbox ? { ...glyphMeta.bbox } : emptyRect());
@@ -727,14 +762,28 @@ function createGlyphShape(shape, glyphPosition) {
727
762
  width: bbox.w,
728
763
  bbox
729
764
  },
730
- edgeEpsilon: shape.edgeEpsilon
765
+ edgeEpsilon: shape.edgeEpsilon,
766
+ rawData: sourceGlyph && shape.raw.fontInstance ? {
767
+ fontInstance: shape.raw.fontInstance,
768
+ sourceLayout: {
769
+ glyphs: [sourceGlyph],
770
+ metrics: {
771
+ x: glyphMeta?.x ?? shape.metrics?.x ?? 0,
772
+ y: glyphMeta?.y ?? shape.metrics?.y ?? 0,
773
+ size: glyphMeta?.size ?? shape.metrics?.size ?? 0,
774
+ width: glyphMeta?.bbox?.w ?? bbox.w
775
+ }
776
+ },
777
+ sourceOptions: shape.raw.sourceOptions ?? null,
778
+ variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
779
+ } : void 0
731
780
  });
732
781
  }
733
782
  function createDerivedShape(shape, parts) {
734
- const copiedParts = copyParts(parts);
735
- const bbox = combineRects(copiedParts.map((part) => part.bbox)) ?? emptyRect();
783
+ const bbox = combineRects(parts.map((part) => part.bbox)) ?? emptyRect();
784
+ const partsByGlyphPosition = groupPartsByGlyphPosition(parts);
736
785
  const glyphs = (shape.raw.glyphs ?? []).map((glyph, glyphPosition) => {
737
- const glyphParts = copiedParts.filter((part) => part.glyphPosition === glyphPosition);
786
+ const glyphParts = partsByGlyphPosition.get(glyphPosition) ?? [];
738
787
  return {
739
788
  ...glyph,
740
789
  bbox: combineRects(glyphParts.map((part) => part.bbox)) ?? (glyph.bbox ? { ...glyph.bbox } : emptyRect()),
@@ -743,7 +792,7 @@ function createDerivedShape(shape, parts) {
743
792
  });
744
793
  return new PAShape({
745
794
  text: shape.text,
746
- parts: copiedParts,
795
+ parts,
747
796
  contours: [],
748
797
  glyphs,
749
798
  bbox,
@@ -751,7 +800,11 @@ function createDerivedShape(shape, parts) {
751
800
  ...shape.metrics,
752
801
  bbox
753
802
  },
754
- edgeEpsilon: shape.edgeEpsilon
803
+ edgeEpsilon: shape.edgeEpsilon,
804
+ rawData: {
805
+ ...shape.raw,
806
+ variantOptions: { ...shape.raw.variantOptions ?? zeroShapeVariant() }
807
+ }
755
808
  });
756
809
  }
757
810
  function createRegionCollection(shape) {
@@ -780,6 +833,16 @@ function copyContours(contours) {
780
833
  ring: copyRing(contour.ring)
781
834
  }));
782
835
  }
836
+ function groupPartsByGlyphPosition(parts) {
837
+ const grouped = /* @__PURE__ */ new Map();
838
+ parts.forEach((part) => {
839
+ if (typeof part.glyphPosition !== "number") return;
840
+ const bucket = grouped.get(part.glyphPosition) ?? [];
841
+ bucket.push(part);
842
+ grouped.set(part.glyphPosition, bucket);
843
+ });
844
+ return grouped;
845
+ }
783
846
  function extractGlyphText(text, glyphPosition, glyphCount) {
784
847
  if (glyphCount <= 1) return text;
785
848
  return Array.from(text ?? "")[glyphPosition] ?? "";
@@ -808,6 +871,19 @@ function normalizeShapeOptions(options = {}) {
808
871
  openWidth: normalizePositive(options.openWidth, 0)
809
872
  };
810
873
  }
874
+ function extractShapeSourceOptions(opts) {
875
+ return {
876
+ size: opts.size,
877
+ flatten: opts.flatten,
878
+ edgeEpsilon: opts.edgeEpsilon,
879
+ kerning: opts.kerning,
880
+ letterSpacing: opts.letterSpacing,
881
+ tracking: opts.tracking,
882
+ script: opts.script,
883
+ language: opts.language,
884
+ features: opts.features
885
+ };
886
+ }
811
887
  function resolveShapeVariant(shape, options = {}) {
812
888
  const normalized = normalizeShapeOptions(options);
813
889
  if (normalized.step <= 0 && normalized.openWidth <= 0) return shape;
@@ -820,9 +896,51 @@ function resolveShapeVariant(shape, options = {}) {
820
896
  shape._cache.shapes.set(cacheKey, next);
821
897
  return next;
822
898
  }
899
+ function deriveGlyphGeometryVariant(geometry, options = {}) {
900
+ const normalized = normalizeShapeOptions(options);
901
+ if (normalized.step <= 0 && normalized.openWidth <= 0) return geometry;
902
+ let parts = geometry.parts;
903
+ if (normalized.openWidth > 0) {
904
+ const geometryEpsilon = resolveGeometryEpsilon(normalized.openWidth);
905
+ parts = parts.map((part) => openPartWithSlit(part, normalized.openWidth, geometryEpsilon));
906
+ }
907
+ if (normalized.step > 0) parts = parts.map((part) => resamplePart(part, normalized.step));
908
+ return {
909
+ ...geometry,
910
+ contours: [],
911
+ parts,
912
+ bbox: combineRects(parts.map((part) => part.bbox)) ?? emptyRect()
913
+ };
914
+ }
823
915
  function toCacheKey(value) {
824
916
  return normalizePositive(value, 0).toFixed(6);
825
917
  }
918
+ function zeroShapeVariant() {
919
+ return {
920
+ openWidth: 0,
921
+ step: 0
922
+ };
923
+ }
924
+ function createGlyphDerivedShape(shape, variant) {
925
+ if (!canUseGlyphDerivedShape(shape)) return null;
926
+ return createTextShape({
927
+ text: shape.text,
928
+ glyphs: shape.raw.sourceLayout.glyphs,
929
+ metrics: shape.raw.sourceLayout.metrics
930
+ }, {
931
+ ...shape.raw.sourceOptions,
932
+ ...mergeShapeVariantOptions(shape.raw.variantOptions, variant)
933
+ }, shape.raw.fontInstance);
934
+ }
935
+ function canUseGlyphDerivedShape(shape) {
936
+ return shape.raw?.fontInstance != null && shape.raw?.sourceOptions != null && shape.raw?.sourceLayout != null && Array.isArray(shape.raw.sourceLayout.glyphs);
937
+ }
938
+ function mergeShapeVariantOptions(previous, next) {
939
+ return {
940
+ openWidth: normalizePositive(next.openWidth, previous?.openWidth ?? 0),
941
+ step: normalizePositive(next.step, previous?.step ?? 0)
942
+ };
943
+ }
826
944
  function resolveGeometryEpsilon(width) {
827
945
  return Math.max(1e-4, normalizePositive(width, 1) * .001);
828
946
  }
@@ -3184,9 +3302,11 @@ var DEFAULT_LINE_HEIGHT_RATIO = 1.2;
3184
3302
  var HUGE_LAYOUT_WIDTH = 1e9;
3185
3303
  var JUSTIFY_EPSILON = 1e-6;
3186
3304
  var QUOTE_RE = /"/g;
3305
+ var SHARED_MEASURE_CACHE_LIMIT = 2048;
3187
3306
  var sharedMeasureContext = null;
3188
3307
  var sharedWordSegmenter = null;
3189
3308
  var sharedGraphemeSegmenter = null;
3309
+ var sharedMeasureCaches = /* @__PURE__ */ new WeakMap();
3190
3310
  function layoutParagraph(fontInstance, text, options = {}, state = {}) {
3191
3311
  const normalized = normalizeParagraphOptions(fontInstance, options);
3192
3312
  const textValue = String(text ?? "");
@@ -3866,15 +3986,24 @@ function createTextMeasurer(fontInstance, options) {
3866
3986
  const cache = /* @__PURE__ */ new Map();
3867
3987
  const openTypeMeasurer = createOpenTypeMeasurer(fontInstance, options);
3868
3988
  const context = getMeasureContext();
3989
+ const sharedCache = getSharedMeasureCache(fontInstance, "canvas");
3990
+ const sharedKeyPrefix = `${options.font}\u0000`;
3869
3991
  return (value) => {
3870
3992
  if (value.length === 0) return 0;
3871
3993
  if (cache.has(value)) return cache.get(value);
3994
+ const sharedKey = `${sharedKeyPrefix}${value}`;
3995
+ const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
3996
+ if (sharedWidth != null) {
3997
+ cache.set(value, sharedWidth);
3998
+ return sharedWidth;
3999
+ }
3872
4000
  let width;
3873
4001
  if (context) {
3874
4002
  context.font = options.font;
3875
4003
  width = context.measureText(value).width;
3876
4004
  } else width = openTypeMeasurer(value);
3877
4005
  cache.set(value, width);
4006
+ writeSharedMeasureCache(sharedCache, sharedKey, width);
3878
4007
  return width;
3879
4008
  };
3880
4009
  }
@@ -3887,6 +4016,7 @@ function createLazyTextMeasurer(fontInstance, options) {
3887
4016
  }
3888
4017
  function createOpenTypeMeasurer(fontInstance, options) {
3889
4018
  const cache = /* @__PURE__ */ new Map();
4019
+ const sharedCache = getSharedMeasureCache(fontInstance, "openType");
3890
4020
  const widthOptions = {
3891
4021
  x: 0,
3892
4022
  y: 0,
@@ -3900,11 +4030,19 @@ function createOpenTypeMeasurer(fontInstance, options) {
3900
4030
  language: options.language,
3901
4031
  features: options.features
3902
4032
  };
4033
+ const sharedKeyPrefix = createOpenTypeMeasureKeyPrefix(widthOptions);
3903
4034
  return (value) => {
3904
4035
  if (value.length === 0) return 0;
3905
4036
  if (cache.has(value)) return cache.get(value);
4037
+ const sharedKey = `${sharedKeyPrefix}${value}`;
4038
+ const sharedWidth = readSharedMeasureCache(sharedCache, sharedKey);
4039
+ if (sharedWidth != null) {
4040
+ cache.set(value, sharedWidth);
4041
+ return sharedWidth;
4042
+ }
3906
4043
  const width = measureAdvanceWidth(fontInstance.font, value, widthOptions);
3907
4044
  cache.set(value, width);
4045
+ writeSharedMeasureCache(sharedCache, sharedKey, width);
3908
4046
  return width;
3909
4047
  };
3910
4048
  }
@@ -3920,6 +4058,49 @@ function getMeasureContext() {
3920
4058
  }
3921
4059
  return null;
3922
4060
  }
4061
+ function getSharedMeasureCache(fontInstance, bucket) {
4062
+ if (fontInstance == null || typeof fontInstance !== "object" && typeof fontInstance !== "function") return null;
4063
+ let caches = sharedMeasureCaches.get(fontInstance);
4064
+ if (!caches) {
4065
+ caches = {
4066
+ canvas: /* @__PURE__ */ new Map(),
4067
+ openType: /* @__PURE__ */ new Map()
4068
+ };
4069
+ sharedMeasureCaches.set(fontInstance, caches);
4070
+ }
4071
+ return caches[bucket];
4072
+ }
4073
+ function readSharedMeasureCache(cache, key) {
4074
+ if (!cache || !cache.has(key)) return null;
4075
+ const value = cache.get(key);
4076
+ cache.delete(key);
4077
+ cache.set(key, value);
4078
+ return value;
4079
+ }
4080
+ function writeSharedMeasureCache(cache, key, value) {
4081
+ if (!cache) return;
4082
+ if (cache.has(key)) cache.delete(key);
4083
+ cache.set(key, value);
4084
+ if (cache.size > SHARED_MEASURE_CACHE_LIMIT) {
4085
+ const oldestKey = cache.keys().next().value;
4086
+ cache.delete(oldestKey);
4087
+ }
4088
+ }
4089
+ function createOpenTypeMeasureKeyPrefix(options) {
4090
+ return [
4091
+ options.size,
4092
+ options.kerning ? 1 : 0,
4093
+ options.letterSpacing ?? "",
4094
+ options.tracking ?? "",
4095
+ options.script ?? "",
4096
+ options.language ?? "",
4097
+ serializeMeasureFeatures(options.features)
4098
+ ].join("|") + "\0";
4099
+ }
4100
+ function serializeMeasureFeatures(features) {
4101
+ if (!features || typeof features !== "object") return "";
4102
+ return Object.keys(features).sort().map((key) => `${key}:${features[key]}`).join(",");
4103
+ }
3923
4104
  function getWordSegmenter() {
3924
4105
  if (sharedWordSegmenter == null) sharedWordSegmenter = new Intl.Segmenter(void 0, { granularity: "word" });
3925
4106
  return sharedWordSegmenter;
@@ -4015,6 +4196,10 @@ var paParagraph = class paParagraph {
4015
4196
  const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegions()");
4016
4197
  return this._getBaseShape(layout).toRegions(shapeOptions);
4017
4198
  }
4199
+ toRegionViews(options = {}) {
4200
+ const { layout = "current", ...shapeOptions } = normalizeParagraphShapeOptions(options, "toRegionViews()");
4201
+ return this._getBaseShape(layout).toRegionViews(shapeOptions);
4202
+ }
4018
4203
  toPoints(options = {}) {
4019
4204
  const { layout = "current", ...pointOptions } = normalizeParagraphPointOptions(options);
4020
4205
  return this._getBaseShape(layout).toPoints(pointOptions);
@@ -4150,6 +4335,7 @@ var paFont = class paFont {
4150
4335
  this.canvasFamily = this.family;
4151
4336
  this._glyphTopologyCache = /* @__PURE__ */ new Map();
4152
4337
  this._glyphFlatCache = /* @__PURE__ */ new Map();
4338
+ this._glyphVariantCache = /* @__PURE__ */ new Map();
4153
4339
  }
4154
4340
  static async load(source, options = {}) {
4155
4341
  const opts = normalizeLoadOptions(options);
@@ -4227,10 +4413,27 @@ var paFont = class paFont {
4227
4413
  }
4228
4414
  return this._glyphFlatCache.get(key);
4229
4415
  }
4416
+ _getGlyphGeometryVariant(glyph, opts) {
4417
+ const openWidth = normalizeShapeVariantValue(opts.openWidth);
4418
+ const step = normalizeShapeVariantValue(opts.step);
4419
+ if (openWidth <= 0 && step <= 0) return this._getFlattenedGlyph(glyph, opts);
4420
+ const key = `${glyph.index}:${opts.size}:${opts.flatten}:${toShapeVariantKey(openWidth)}:${toShapeVariantKey(step)}`;
4421
+ if (!this._glyphVariantCache.has(key)) this._glyphVariantCache.set(key, deriveGlyphGeometryVariant(this._getFlattenedGlyph(glyph, opts), {
4422
+ openWidth,
4423
+ step
4424
+ }));
4425
+ return this._glyphVariantCache.get(key);
4426
+ }
4230
4427
  _layoutText(value, opts) {
4231
4428
  return layoutGlyphs(this.font, value, opts);
4232
4429
  }
4233
4430
  };
4431
+ function normalizeShapeVariantValue(value) {
4432
+ return Number.isFinite(value) && value > 0 ? Number(value) : 0;
4433
+ }
4434
+ function toShapeVariantKey(value) {
4435
+ return normalizeShapeVariantValue(value).toFixed(6);
4436
+ }
4234
4437
  async function fetchFontBytes(source) {
4235
4438
  const response = await fetch(source);
4236
4439
  if (!response.ok) throw new Error(`Failed to load font from ${source}: ${response.status} ${response.statusText}`);