mirror of
https://github.com/google/pebble.git
synced 2025-05-18 01:14:55 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
337
tools/generate_pdcs/json2commands.py
Normal file
337
tools/generate_pdcs/json2commands.py
Normal file
|
@ -0,0 +1,337 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
'''
|
||||
JSON2COMMANDS creates Pebble Draw Commands (the Python Objects, _not_ a serialized .pdc) from a JSON file.
|
||||
Currently only the PathCommand is supported.
|
||||
|
||||
The JSON file can contain multiple frames (i.e. PDC Sequence).
|
||||
Each frame is composed of 'fillGroups'.
|
||||
A fillGroup may be: An individual filled polygon (a.k.a. a fill), or _all_ unfilled polylines (a.k.a. all open paths).
|
||||
Each fillGroup is parsed separately and a list of Pebble Draw Commands that describe it is created.
|
||||
The created list should have the length of the lowest number of commands possible in order to draw that fillGroup.
|
||||
|
||||
Currently, there is no support for a JSON to contain the viewbox size or fill colors.
|
||||
The viewbox size is currently passed in as a parameter.
|
||||
The fill color is currently defaulted to solid white.
|
||||
'''
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import pebble_commands
|
||||
import json
|
||||
import graph
|
||||
from itertools import groupby
|
||||
|
||||
INVISIBLE_POINT_THRESHOLD = 500
|
||||
DISPLAY_DIM_X = 144
|
||||
DISPLAY_DIM_Y = 168
|
||||
OPEN_PATH_TAG = "_"
|
||||
|
||||
|
||||
def parse_color(color_opacity, truncate):
|
||||
if color_opacity is None:
|
||||
return 0
|
||||
|
||||
r = int(round(255 * color_opacity[0]))
|
||||
g = int(round(255 * color_opacity[1]))
|
||||
b = int(round(255 * color_opacity[2]))
|
||||
a = int(round(255 * color_opacity[3]))
|
||||
|
||||
return pebble_commands.convert_color(r, g, b, a, truncate)
|
||||
|
||||
|
||||
def parse_json_line_data(json_line_data, viewbox_size=(DISPLAY_DIM_X, DISPLAY_DIM_Y)):
|
||||
# A list of one-way vectors, but intended to store their negatives at all times.
|
||||
bidirectional_lines = []
|
||||
|
||||
for line_data in json_line_data:
|
||||
# Skip invisible lines
|
||||
if abs(line_data['startPoint'][0]) > INVISIBLE_POINT_THRESHOLD or \
|
||||
abs(line_data['startPoint'][1]) > INVISIBLE_POINT_THRESHOLD or \
|
||||
abs(line_data['endPoint'][0]) > INVISIBLE_POINT_THRESHOLD or \
|
||||
abs(line_data['endPoint'][1]) > INVISIBLE_POINT_THRESHOLD:
|
||||
continue
|
||||
|
||||
# Center the viewbox of all lines (by moving the lines' absolute
|
||||
# coordinates relative to the screen)
|
||||
dx = -(DISPLAY_DIM_X - viewbox_size[0]) / 2
|
||||
dy = -(DISPLAY_DIM_Y - viewbox_size[1]) / 2
|
||||
start_point = (line_data["startPoint"][0] + dx, line_data["startPoint"][1] + dy)
|
||||
end_point = (line_data["endPoint"][0] + dx, line_data["endPoint"][1] + dy)
|
||||
|
||||
# Since lines are represented and stored as one-way vectors, but may be
|
||||
# drawn in either direction, all operations must be done on their reverse
|
||||
line = (start_point, end_point)
|
||||
reverse_line = (end_point, start_point)
|
||||
|
||||
# Skip duplicate lines
|
||||
if line in bidirectional_lines:
|
||||
continue
|
||||
|
||||
bidirectional_lines.append(line)
|
||||
bidirectional_lines.append(reverse_line)
|
||||
|
||||
return bidirectional_lines
|
||||
|
||||
|
||||
def determine_longest_path(bidirectional_lines):
|
||||
'''
|
||||
Returns the longest path in 'bidirectional_lines', and removes all its segments from 'bidirectional_lines'
|
||||
If 'bidirectional_lines' contains more than one possible longest path, only one will be returned.
|
||||
'''
|
||||
# Construct graph out of bidirectional_lines
|
||||
g = graph.Graph({})
|
||||
for line in bidirectional_lines:
|
||||
g.add_edge(line)
|
||||
|
||||
# Find longest path
|
||||
longest_path_length = 0
|
||||
longest_path = []
|
||||
vertices = g.get_vertices()
|
||||
for i in range(len(vertices)):
|
||||
start_vertex = vertices[i]
|
||||
|
||||
for j in range(i, len(vertices)):
|
||||
end_vertex = vertices[j]
|
||||
paths = g.find_all_paths(start_vertex, end_vertex)
|
||||
for path in paths:
|
||||
if (len(path) - 1) > longest_path_length:
|
||||
longest_path = path
|
||||
longest_path_length = len(path) - 1
|
||||
|
||||
# Edge case - Line is a point
|
||||
if len(longest_path) == 1:
|
||||
longest_path = [longest_path, longest_path]
|
||||
|
||||
# Remove longest_path's line segments from bidirectional_lines
|
||||
# Since bidirectional_lines is a list of one-way vectors but represents
|
||||
# bidirectional lines, a line segment and its reverse must be removed to
|
||||
# keep its integrity
|
||||
for k in range(len(longest_path) - 1):
|
||||
path_line = (longest_path[k], longest_path[k + 1])
|
||||
reverse_path_line = (path_line[1], path_line[0])
|
||||
bidirectional_lines.remove(path_line)
|
||||
bidirectional_lines.remove(reverse_path_line)
|
||||
|
||||
return longest_path
|
||||
|
||||
|
||||
def process_unique_group_of_lines(unique_group_data, translate, viewbox_size, path_open, stroke_width, stroke_color, fill_color, precise, raise_error):
|
||||
'''
|
||||
Creates a list of commands that draw out a unique group of lines.
|
||||
|
||||
A unique group of lines is defined as having a unique stroke width, stroke color, and fill.
|
||||
Note that this does _not_ guarantee the group may be described by a single Pebble Draw Command.
|
||||
'''
|
||||
|
||||
unique_group_commands = []
|
||||
|
||||
bidirectional_lines = parse_json_line_data(unique_group_data, viewbox_size)
|
||||
if not bidirectional_lines:
|
||||
return unique_group_commands
|
||||
|
||||
while bidirectional_lines:
|
||||
longest_path = determine_longest_path(bidirectional_lines)
|
||||
|
||||
try:
|
||||
c = pebble_commands.PathCommand(longest_path,
|
||||
path_open,
|
||||
translate,
|
||||
stroke_width,
|
||||
stroke_color,
|
||||
fill_color,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if c is not None:
|
||||
unique_group_commands.append(c)
|
||||
except pebble_commands.InvalidPointException:
|
||||
raise
|
||||
|
||||
return unique_group_commands
|
||||
|
||||
|
||||
def process_fill(fillGroup_data, translate, viewbox_size, path_open, precise, raise_error, truncate_color):
|
||||
fill_command = []
|
||||
error = False
|
||||
|
||||
# A fill is implicitly a unique group of lines - all line segments must have the same stroke width, stroke color
|
||||
# Get line style from first line segment
|
||||
stroke_width = fillGroup_data[0]['thickness']
|
||||
stroke_color = parse_color(fillGroup_data[0]['color'], truncate_color)
|
||||
# Fill color should be solid white until it can be inserted in the JSON
|
||||
fill_color = parse_color([1, 1, 1, 1], truncate_color)
|
||||
if stroke_color == 0:
|
||||
stroke_width = 0
|
||||
elif stroke_width == 0:
|
||||
stroke_color = 0
|
||||
|
||||
try:
|
||||
unique_group_commands = process_unique_group_of_lines(
|
||||
fillGroup_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
stroke_width,
|
||||
stroke_color,
|
||||
fill_color,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if unique_group_commands:
|
||||
fill_command += unique_group_commands
|
||||
except pebble_commands.InvalidPointException:
|
||||
error = True
|
||||
|
||||
return fill_command, error
|
||||
|
||||
|
||||
def process_open_paths(fillGroup_data, translate, viewbox_size, path_open, precise, raise_error, truncate_color):
|
||||
open_paths_commands = []
|
||||
error = False
|
||||
|
||||
fill_color = parse_color([0, 0, 0, 0], truncate_color) # No fill color
|
||||
|
||||
# These open paths are part of the same fillGroup, but may have varied stroke width
|
||||
fillGroup_data = sorted(fillGroup_data, key=lambda a: a['thickness'])
|
||||
for stroke_width, unique_width_group in groupby(fillGroup_data, lambda c: c['thickness']):
|
||||
unique_width_data = list(unique_width_group)
|
||||
|
||||
# These open paths have the same width, but may have varied color
|
||||
unique_width_data = sorted(unique_width_data, key=lambda d: d['color'])
|
||||
for stroke_color_raw, unique_width_and_color_group in groupby(unique_width_data, lambda e: e['color']):
|
||||
# These are a unique group of lines
|
||||
unique_width_and_color_data = list(unique_width_and_color_group)
|
||||
|
||||
stroke_color = parse_color(stroke_color_raw, truncate_color)
|
||||
if stroke_color == 0:
|
||||
stroke_width = 0
|
||||
elif stroke_width == 0:
|
||||
stroke_color = 0
|
||||
|
||||
try:
|
||||
unique_group_commands = process_unique_group_of_lines(
|
||||
unique_width_and_color_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
stroke_width,
|
||||
stroke_color,
|
||||
fill_color,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if unique_group_commands:
|
||||
open_paths_commands += unique_group_commands
|
||||
except pebble_commands.InvalidPointException:
|
||||
error = True
|
||||
|
||||
return open_paths_commands, error
|
||||
|
||||
|
||||
def get_commands(translate, viewbox_size, frame_data, precise=False, raise_error=False, truncate_color=True):
|
||||
commands = []
|
||||
errors = []
|
||||
|
||||
fillGroups_data = frame_data['lineData']
|
||||
|
||||
# The 'fillGroup' property describes the type of group: A unique letter
|
||||
# (e.g. "A", "B", "C" etc.) for a unique fill, and a special identifier
|
||||
# for ALL open paths (non-fills)
|
||||
only_fills = list([d for d in fillGroups_data if d["fillGroup"] != OPEN_PATH_TAG])
|
||||
only_fills = sorted(only_fills, key=lambda f: f["fillGroup"]) # Don't assume data is sorted
|
||||
only_open_paths = list([d for d in fillGroups_data if d["fillGroup"] == OPEN_PATH_TAG])
|
||||
# Fills must be drawn before open paths, so place them first
|
||||
ordered_fill_groups = only_fills + only_open_paths
|
||||
|
||||
# Process fillGroups
|
||||
for path_type, fillGroup in groupby(ordered_fill_groups, lambda b: b['fillGroup']):
|
||||
fillGroup_data = list(fillGroup)
|
||||
|
||||
path_open = path_type == '_'
|
||||
if not path_open:
|
||||
# Filled fillGroup
|
||||
fillGroup_commands, error = process_fill(
|
||||
fillGroup_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
precise,
|
||||
raise_error,
|
||||
truncate_color)
|
||||
else:
|
||||
# Open path fillGroup
|
||||
fillGroup_commands, error = process_open_paths(
|
||||
fillGroup_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
precise,
|
||||
raise_error,
|
||||
truncate_color)
|
||||
|
||||
if error:
|
||||
errors += str(path_type)
|
||||
elif fillGroup_commands:
|
||||
commands += fillGroup_commands
|
||||
|
||||
if not commands:
|
||||
# Insert one 'invisible' command so the frame is valid
|
||||
c = pebble_commands.PathCommand([((0.0), (0.0)), ((0.0), (0.0))],
|
||||
True,
|
||||
translate,
|
||||
0,
|
||||
0,
|
||||
0)
|
||||
commands.append(c)
|
||||
|
||||
return commands, errors
|
||||
|
||||
|
||||
def parse_json_sequence(filename, viewbox_size, precise=False, raise_error=False):
|
||||
frames = []
|
||||
errors = []
|
||||
translate = (0, 0)
|
||||
|
||||
with open(filename) as json_file:
|
||||
try:
|
||||
data = json.load(json_file)
|
||||
except ValueError:
|
||||
print('Invalid JSON format')
|
||||
return frames, 0, 0
|
||||
|
||||
frames_data = data['lineData']
|
||||
frame_duration = int(data['compData']['frameDuration'] * 1000)
|
||||
for idx, frame_data in enumerate(frames_data):
|
||||
cmd_list, frame_errors = get_commands(
|
||||
translate,
|
||||
viewbox_size,
|
||||
frame_data,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if frame_errors:
|
||||
errors.append((idx, frame_errors))
|
||||
elif cmd_list is not None:
|
||||
frames.append(cmd_list)
|
||||
|
||||
return frames, errors, frame_duration
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('path', type=str, help="Path to json file")
|
||||
args = parser.parse_args()
|
||||
path = os.path.abspath(args.path)
|
||||
parse_json_sequence(path)
|
Loading…
Add table
Add a link
Reference in a new issue