diff --git a/sources/phases/main.py b/sources/phases/main.py index 1db3dcb..feaf80a 100644 --- a/sources/phases/main.py +++ b/sources/phases/main.py @@ -1762,6 +1762,99 @@ def rotate_diacritics( return [lookup] +def avoid_abrupt_inflections( + builder: Builder, + original_schemas: OrderedSet[Schema], + schemas: OrderedSet[Schema], + new_schemas: OrderedSet[Schema], + classes: PrefixView[FreezableList[Schema]], + named_lookups: PrefixView[Lookup], + add_rule: AddRule, +) -> Sequence[Lookup]: + lookup = Lookup( + 'rclt', + 'dflt', + flags=fontTools.otlLib.builder.LOOKUP_FLAG_IGNORE_MARKS, + ) + if len(original_schemas) != len(schemas): + return [lookup] + named_lookups['smooth_1'] = Lookup(None, None) + named_lookups['smooth_2'] = Lookup(None, None) + to_do_i_first = collections.defaultdict(list) + to_do_a_first = collections.defaultdict(list) + to_do_i_second = collections.defaultdict(list) + to_do_a_second = collections.defaultdict(list) + for schema in new_schemas: + if (schema.glyph_class != GlyphClass.JOINER or not isinstance(schema.path, Curve) or schema.path.angle_in % 90 != 0 or schema.path.angle_out % 90 != 0 + or schema.path.reversed_circle or schema.path.stretch != 0 or schema.path.entry_position != 1 or schema.path.exit_position != 1 + ): + continue + if (schema.path.angle_in - schema.path.angle_out) % 360 == 180: + if not schema.diphthong_1: + to_do_i_first[schema.path.clockwise, float(schema.path.angle_out)].append(schema) + if not schema.diphthong_2: + to_do_i_second[not schema.path.clockwise, float(schema.path.angle_in)].append(schema) + if not schema.diphthong_1 and not schema.diphthong_2 and not issubclass(schema.original_shape, Curve): + da = schema.path.angle_out - schema.path.angle_in + if schema.path.clockwise: + da *= -1 + da %= 360 + if 270 <= da < 315: + to_do_a_first[schema.path.clockwise, float(schema.path.angle_out)].append(schema) + to_do_a_second[not schema.path.clockwise, float(schema.path.angle_in)].append(schema) + for (c_i, a_i), schemas_i in to_do_i_first.items(): + for (c_a, a_a), schemas_a in to_do_a_second.items(): + if c_i != c_a or a_i != a_a: + continue + class_name_i = f'IA_i_{c_i}_{a_i}' + class_name_a = f'IA_a_{c_a}_{a_a}' + class_name_s2_input = f'i_{class_name_i}_s2' + class_name_s1_input = f'i_{class_name_i}_s1' + class_name_s2_output = f'o_{class_name_i}_s2' + class_name_s1_output = f'o_{class_name_i}_s1' + classes[class_name_i].extend(schemas_i) + classes[class_name_a].extend(schemas_a) + for schema_i in schemas_i: + assert isinstance(schema_i.path, Curve) + schema_i_2 = schema_i.clone(cmap=None, path=schema_i.path.clone(smooth_1=True)) + classes[class_name_s2_input].append(schema_i) + classes[class_name_s2_output].append(schema_i_2) + add_rule(named_lookups['smooth_1'], Rule(class_name_s2_input, class_name_s2_output)) + for schema_a in schemas_a: + assert isinstance(schema_a.path, Curve) + schema_a_1 = schema_a.clone(cmap=None, path=schema_a.path.clone(smooth_2=True)) + classes[class_name_s1_input].append(schema_a) + classes[class_name_s1_output].append(schema_a_1) + add_rule(named_lookups['smooth_2'], Rule(class_name_s1_input, class_name_s1_output)) + add_rule(lookup, Rule([], [class_name_i, class_name_a], [], lookups=['smooth_1', 'smooth_2'])) + for (c_a, a_a), schemas_a in to_do_a_first.items(): + for (c_i, a_i), schemas_i in to_do_i_second.items(): + if c_i != c_a or a_i != a_a: + continue + class_name_i = f'AI_i_{c_i}_{a_i}' + class_name_a = f'AI_a_{c_a}_{a_a}' + class_name_s2_input = f'i_{class_name_i}_s2' + class_name_s1_input = f'i_{class_name_i}_s1' + class_name_s2_output = f'o_{class_name_i}_s2' + class_name_s1_output = f'o_{class_name_i}_s1' + classes[class_name_i].extend(schemas_i) + classes[class_name_a].extend(schemas_a) + for schema_i in schemas_i: + assert isinstance(schema_i.path, Curve) + schema_i_1 = schema_i.clone(cmap=None, path=schema_i.path.clone(smooth_2=True)) + classes[class_name_s1_input].append(schema_i) + classes[class_name_s1_output].append(schema_i_1) + add_rule(named_lookups['smooth_2'], Rule(class_name_s1_input, class_name_s1_output)) + for schema_a in schemas_a: + assert isinstance(schema_a.path, Curve) + schema_a_2 = schema_a.clone(cmap=None, path=schema_a.path.clone(smooth_1=True)) + classes[class_name_s2_input].append(schema_a) + classes[class_name_s2_output].append(schema_a_2) + add_rule(named_lookups['smooth_1'], Rule(class_name_s2_input, class_name_s2_output)) + add_rule(lookup, Rule([], [class_name_a, class_name_i], [], lookups=['smooth_1', 'smooth_2'])) + return [lookup] + + def shade( builder: Builder, original_schemas: OrderedSet[Schema], @@ -1943,6 +2036,7 @@ def classify_marks_for_trees( join_double_marks, separate_subantiparallel_lines, rotate_diacritics, + avoid_abrupt_inflections, shade, create_superscripts_and_subscripts, make_widthless_variants_of_marks, diff --git a/sources/shapes.py b/sources/shapes.py index 515af63..19fe5e0 100644 --- a/sources/shapes.py +++ b/sources/shapes.py @@ -1921,6 +1921,10 @@ class Curve(Shape): curve letter is in a context where it would look confusingly like a loop, i.e. like a circle letter, it can be clearer to shift the exit point earlier. + smooth_1: Whether the exit part of this curve is modified to + create a more gradual inflection with the following shape. + smooth_2: Whether the entry part of this curve is modified to + create a more gradual inflection with the preceding shape. """ @override @@ -1940,6 +1944,8 @@ def __init__( may_reposition_cursive_endpoints: bool = False, entry_position: float = 1, exit_position: float = 1, + smooth_1: bool = False, + smooth_2: bool = False, ) -> None: """Initializes this `Curve`. @@ -1958,6 +1964,8 @@ def __init__( may_reposition_cursive_endpoints: The ``may_reposition_cursive_endpoints`` attribute. entry_position: The ``entry_position`` attribute. exit_position: The ``exit_position`` attribute. + smooth_1: The ``smooth_1`` attribute. + smooth_2: The ``smooth_2`` attribute. """ assert overlap_angle is None or abs(angle_out - angle_in) == 180, 'Only a semicircle may have an overlap angle' assert stretch > -1 @@ -1965,6 +1973,7 @@ def __init__( assert exit_position == 1 or may_reposition_cursive_endpoints, f'{exit_position=}' assert 0 <= entry_position <= 1 assert 0 <= exit_position <= 1 + assert not (smooth_1 and smooth_2) self.angle_in: Final = angle_in self.angle_out: Final = angle_out self.clockwise: Final = clockwise @@ -1978,6 +1987,8 @@ def __init__( self.may_reposition_cursive_endpoints: Final = may_reposition_cursive_endpoints self.entry_position: Final = entry_position self.exit_position: Final = exit_position + self.smooth_1: Final = smooth_1 + self.smooth_2: Final = smooth_2 @override def clone( @@ -1996,6 +2007,8 @@ def clone( may_reposition_cursive_endpoints: bool | CloneDefault = CLONE_DEFAULT, entry_position: float | CloneDefault = CLONE_DEFAULT, exit_position: float | CloneDefault = CLONE_DEFAULT, + smooth_1: bool | CloneDefault = CLONE_DEFAULT, + smooth_2: bool | CloneDefault = CLONE_DEFAULT, ) -> Self: return type(self)( self.angle_in if angle_in is CLONE_DEFAULT else angle_in, @@ -2013,25 +2026,37 @@ def clone( ), entry_position=self.entry_position if entry_position is CLONE_DEFAULT else entry_position, exit_position=self.exit_position if exit_position is CLONE_DEFAULT else exit_position, + smooth_1=self.smooth_1 if smooth_1 is CLONE_DEFAULT else smooth_1, + smooth_2=self.smooth_2 if smooth_2 is CLONE_DEFAULT else smooth_2, ) @override def get_name(self, size: float, joining_type: Type) -> str: if self.overlap_angle is not None: - return f'{int(self.overlap_angle)}' - if joining_type != Type.ORIENTING: - return '' - return f'''{ - int(self.angle_in) - }{ - 'n' if self.clockwise else 'p' - }{ - int(self.angle_out) - }{ - 'r' if self.reversed_circle else '' - }{ - '.ee' if not self.entry_position == self.exit_position == 1 else '' - }''' + name = f'{int(self.overlap_angle)}' + elif joining_type == Type.ORIENTING: + name = f'''{ + int(self.angle_in) + }{ + 'n' if self.clockwise else 'p' + }{ + int(self.angle_out) + }{ + 'r' if self.reversed_circle else '' + }{ + '.ee' if not self.entry_position == self.exit_position == 1 else '' + }''' + else: + name = '' + if self.smooth_1 or self.smooth_2: + name += f'''{ + '.' if name else '' + }s{ + '1' if self.smooth_1 else '' + }{ + '2' if self.smooth_2 else '' + }''' + return name @override def group(self) -> Hashable: @@ -2060,6 +2085,8 @@ def group(self) -> Hashable: self.overlap_angle, self.entry_position, self.exit_position, + self.smooth_1, + self.smooth_2, ) @override @@ -2118,8 +2145,8 @@ def _pre_stretch_values(self) -> tuple[float, float, float, float, float]: def get_normalized_angles( self, - diphthong_1: bool = False, - diphthong_2: bool = False, + offset_1: float = 0, + offset_2: float = 0, angle_in: float | None = None, angle_out: float | None = None, ) -> tuple[float, float]: @@ -2127,10 +2154,8 @@ def get_normalized_angles( angle_in = self.angle_in if angle_out is None: angle_out = self.angle_out - if diphthong_1: - angle_out = (angle_out + 90 * (1 if self.clockwise else -1)) % 360 - if diphthong_2: - angle_in = (angle_in - 90 * (1 if self.clockwise else -1)) % 360 + angle_out = (angle_out + offset_1 * (1 if self.clockwise else -1)) % 360 + angle_in = (angle_in - offset_2 * (1 if self.clockwise else -1)) % 360 if self.clockwise and angle_out > angle_in: angle_out -= 360 elif not self.clockwise and angle_out < angle_in: @@ -2141,14 +2166,14 @@ def get_normalized_angles( def _get_normalized_angles_and_da( self, - diphthong_1: bool, - diphthong_2: bool, + offset_1: float, + offset_2: float, final_circle_diphthong: bool, initial_circle_diphthong: bool, angle_in: float | None = None, angle_out: float | None = None, ) -> tuple[float, float, float]: - a1, a2 = self.get_normalized_angles(diphthong_1, diphthong_2, angle_in, angle_out) + a1, a2 = self.get_normalized_angles(offset_1, offset_2, angle_in, angle_out) if final_circle_diphthong: a2 = a1 elif initial_circle_diphthong: @@ -2172,7 +2197,7 @@ def get_da( The difference between this curve’s entry angle and exit angle in the range (0, 360]. """ - return self._get_normalized_angles_and_da(False, False, False, False, angle_in, angle_out)[2] + return self._get_normalized_angles_and_da(0, 0, False, False, angle_in, angle_out)[2] def _get_angle_to_overlap_point( self, @@ -2234,9 +2259,10 @@ def draw( ) -> tuple[float, float, float, float] | None: pen = glyph.glyphPen() pre_stretch_angle_in, pre_stretch_angle_out, stretch_axis_angle, scale_x, scale_y = self._pre_stretch_values + smooth_delta = 45 a1, a2, da = self._get_normalized_angles_and_da( - diphthong_1, - diphthong_2, + 90 if diphthong_1 else smooth_delta if self.smooth_1 else 0, + 90 if diphthong_2 else smooth_delta if self.smooth_2 else 0, final_circle_diphthong, initial_circle_diphthong, pre_stretch_angle_in, @@ -2254,6 +2280,11 @@ def draw( entry = (p0[0] + entry_delta[0], p0[1] + entry_delta[1]) pen.moveTo(entry) pen.lineTo(p0) + elif self.smooth_2: + entry_delta = _rect(-r, math.radians((self.angle_in - smooth_delta * (1 if self.clockwise else -1)) % 360)) + entry = (p0[0] + entry_delta[0], p0[1] + entry_delta[1]) + pen.moveTo(entry) + pen.lineTo(p0) else: entry = p0 pen.moveTo(entry) @@ -2305,6 +2336,10 @@ def draw( exit_delta = _rect(r, math.radians((a2 - 90 * (1 if self.clockwise else -1)) % 360)) exit = (exit[0] + exit_delta[0], exit[1] + exit_delta[1]) pen.lineTo(exit) + elif self.smooth_1: + exit_delta = _rect(r, math.radians((self.angle_out + smooth_delta * (1 if self.clockwise else -1)) % 360)) + exit = (exit[0] + exit_delta[0], exit[1] + exit_delta[1]) + pen.lineTo(exit) pen.endPath() relative_mark_angle = (a1 + a2) / 2 if not anchor and joining_type != Type.NON_JOINING: diff --git a/tests/antichiral-sequences.test b/tests/antichiral-sequences.test index 56aa058..8713738 100644 --- a/tests/antichiral-sequences.test +++ b/tests/antichiral-sequences.test @@ -13,5 +13,10 @@ # limitations under the License. # Semicircle letter before circle letter -1BC4A 1BC41 1BC19::[u1BC4A.ui@0,620|u1BC41.a.270p180@200,520|u1BC19.m@0,0|_@640,0] -1BC4A 1BC44 1BC19::[u1BC4A.ui@0,570|u1BC44.o.270p180@200,420|u1BC19.m@50,0|_@740,0] +1BC4A 1BC41 1BC19::[u1BC4A.ui.s1@0,620|u1BC41.a.270p180.s2@241,520|u1BC19.m@82,0|_@722,0] +1BC4A 1BC44 1BC19::[u1BC4A.ui.s1@0,570|u1BC44.o.270p180.s2@241,420|u1BC19.m@153,0|_@843,0] + +# Circle letter before semicircle letter +1BC1A 1BC41 1BC4B::[u1BC1A.n@100,0|u1BC41.a.180n270.s1@0,0|u1BC4B.ee.s2@241,0|_@722,0] +1BC1A 1BC44 1BC4B::[u1BC1A.n@150,0|u1BC44.o.180n270.s1@0,0|u1BC4B.ee.s2@362,50|_@843,0] +1BC1A 1BC56 1BC4B::[u1BC1A.n@150,0|u1BC44.o.180n270.s1@0,0|dupl_.Dot.0.rel1@150,150|u1BC4B.ee.s2@362,50|_@843,0]